65.9K
CodeProject 正在变化。 阅读更多。
Home

使用 MVVM 模式的视图模型对 Observable Collection 进行排序

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.67/5 (6投票s)

2011年4月7日

CPOL

8分钟阅读

viewsIcon

86639

downloadIcon

1671

演示了如何允许绑定到 ObservableCollection 的控件(例如 Listview)使用 MVVM(model, view, view model)模式进行排序,并且排序在 view model 中进行。

引言

本文的目的是演示如何允许绑定到 ObservableCollection 的控件(例如 Listview)使用 MVVM(model, view, view model)模式进行排序,并且排序在 view model 中进行。

背景

本文的背景是我有一个需求,需要为数据绑定到 ObservableCollection 的 listview 添加排序功能。我发现的主要问题是 ObservableCollection 是一个无序列表,你无法对其进行排序。我花了很大力气才找到一篇能在 MVVM 模式框架中实现此功能的文章,以便排序在 view model 中完成,而不是在代码隐藏中。这样可以使测试更容易,并更清晰地分离我的关注点。

为了帮助未来可能遇到相同问题的任何人,我想写下我在 MVVM 环境中如何实现这一需求。

本文的示例应用程序是一个人名列表,您可以按名字或姓氏进行排序。

为了说明,我们有一个未排序的列表如下

UnSorted.png

点击名字标题,它会按升序排序,如下所示

SortedFirstAscending.png

然后我们再次点击名字,它会按降序排序,如下所示

SortedFirstDescending.png

本文的范围不包括解释 MVVM 模式是什么,以及任何用于实现更完整 MVVM 实现的框架。有关 MVVM 的更多信息,本网站上有一些非常好的文章,它们解释了 MVVM 的概述以及在开发完整应用程序时可以使用哪些框架。

同样不在本文讨论范围内的还有应用程序中加载测试数据的方式。这些数据应该从数据库、文件加载或输入到应用程序中。代码中执行此操作的方式并非最佳实践,仅用于说明排序功能。

排序测试

首先,这是显示排序如何完成的测试。

所有测试代码都可以在 ObservableCollectionSortingExample.Test 项目中找到。

1. using Microsoft.VisualStudio.TestTools.UnitTesting;
2. 
3. namespace ObservableCollectionSortingExample.Test
4. {
5.     [TestClass]
6.     public class SortingTests
7.     {
8.         PeopleVewModel viewModel;
9.         Person person1;
10.         Person person2;
11.         Person person3;
12.         Person person4;
13.         Person person5;
14.         Person person6;
15. 
16.         [TestInitializeAttribute]
17.         public void InitialiseViewModel()
18.         {
19.             viewModel = new PeopleVewModel();
20. 
21.             People people = new People();
22.             person1 = new Person() { Firstname = "Michael", Lastname = "Bookatz" };
23.             people.Add(person1);
24.             person2 = new Person() { Firstname = "Chris", Lastname = "Johnson" };
25.             people.Add(person2);
26.             person3 = new Person() { Firstname = "John", Lastname = "Doe" };
27.             people.Add(person3);
28.             person4 = new Person() { Firstname = "Ann", Lastname = "Other" };
29.             people.Add(person4);
30.             person5 = new Person() { Firstname = "Jack", Lastname = "Smith" };
31.             people.Add(person5);
32.             person6 = new Person() { Firstname = "Charles", Lastname = "Langford" };
33.             people.Add(person6);
34. 
35.             viewModel.People = people;
36.         }
37. 
38.         [TestMethod]
39.         public void SortByFirstname()
40.         {
41.             viewModel.SortList.Execute("Firstname");
42. 
43.             Assert.IsTrue(viewModel.PeopleView.Count == 6);
44.             Assert.IsTrue(((Person)viewModel.PeopleView.GetItemAt(0)) == person4);
45.             Assert.IsTrue(((Person)viewModel.PeopleView.GetItemAt(1)) == person6);
46.             Assert.IsTrue(((Person)viewModel.PeopleView.GetItemAt(2)) == person2);
47.             Assert.IsTrue(((Person)viewModel.PeopleView.GetItemAt(3)) == person5);
48.             Assert.IsTrue(((Person)viewModel.PeopleView.GetItemAt(4)) == person3);
49.             Assert.IsTrue(((Person)viewModel.PeopleView.GetItemAt(5)) == person1);
50. 
51.             viewModel.SortList.Execute("Firstname");
52. 
53.             Assert.IsTrue(viewModel.PeopleView.Count == 6);
54.             Assert.IsTrue(((Person)viewModel.PeopleView.GetItemAt(5)) == person4);
55.             Assert.IsTrue(((Person)viewModel.PeopleView.GetItemAt(4)) == person6);
56.             Assert.IsTrue(((Person)viewModel.PeopleView.GetItemAt(3)) == person2);
57.             Assert.IsTrue(((Person)viewModel.PeopleView.GetItemAt(2)) == person5);
58.             Assert.IsTrue(((Person)viewModel.PeopleView.GetItemAt(1)) == person3);
59.             Assert.IsTrue(((Person)viewModel.PeopleView.GetItemAt(0)) == person1);
60.         }
61.     }
62. }

第 8 至 14 行是用于测试的对象(表示 view model 和一个要测试的人员列表)的字段声明。

在第 11 行,我们创建了将要进行循环迭代的对象。这是一个泛型类型,因此可以用于测试所有不同的类。

在第 16 至 36 行,运行测试初始化。这会设置模型对象,然后创建测试将要运行的 view model。

第 39 行开始实际的测试方法。第 41 和 51 行表示列表被排序。第一组断言(第 43 至 49 行)确保在按名字首次排序后,列表的顺序是正确的升序排列。我们在第 51 行反转列表,然后在第 53 至 50 行检查顺序是否已反转。

测试中的一个重要注意事项是,用于获取 Person 对象与预期 Person 对象进行比较的 view model 属性不是 ObservableCollection,而是 ListCollectionView。这是因为 ObservableCollection 是一个无序的项列表。绕过 ObservableCollection 排序(以及应用分组和过滤)的方法是使用实现 ICollectionView 的类,而 ListCollectionView 就是其中之一。

为了完整起见,下面是按姓氏排序的测试

1. [TestMethod]
2. public void SortByLastname()
3. {
4.     viewModel.SortList.Execute("Lastname");
5. 
6.     Assert.IsTrue(viewModel.PeopleView.Count == 6);
7.     Assert.IsTrue(((Person)viewModel.PeopleView.GetItemAt(0)) == person1);
8.     Assert.IsTrue(((Person)viewModel.PeopleView.GetItemAt(1)) == person3);
9.     Assert.IsTrue(((Person)viewModel.PeopleView.GetItemAt(2)) == person2);
10.    Assert.IsTrue(((Person)viewModel.PeopleView.GetItemAt(3)) == person6);
11.    Assert.IsTrue(((Person)viewModel.PeopleView.GetItemAt(4)) == person4);
12.    Assert.IsTrue(((Person)viewModel.PeopleView.GetItemAt(5)) == person5);
13. 
14.    viewModel.SortList.Execute("Lastname");
15. 
16.    Assert.IsTrue(viewModel.PeopleView.Count == 6);
17.    Assert.IsTrue(((Person)viewModel.PeopleView.GetItemAt(5)) == person1);
18.    Assert.IsTrue(((Person)viewModel.PeopleView.GetItemAt(4)) == person3);
19.    Assert.IsTrue(((Person)viewModel.PeopleView.GetItemAt(3)) == person2);
20.    Assert.IsTrue(((Person)viewModel.PeopleView.GetItemAt(2)) == person6);
21.    Assert.IsTrue(((Person)viewModel.PeopleView.GetItemAt(1)) == person4);
22.    Assert.IsTrue(((Person)viewModel.PeopleView.GetItemAt(0)) == person5);
23. }

这与上面的测试相同,唯一的区别是第 4 行和第 14 行,您按姓氏而不是名字进行排序。

模型

模型的所有代码都可以在 ObservableCollectionSortingExample.Model 中找到。

这里使用的模型非常简单。Person 类仅定义两个 string 属性:名字和姓氏,然后是相等性覆盖,以便我们可以在测试中检查两个 Person 对象是否相等。代码如下

1. public class Person
2. {
3.     public string Firstname { get; set; }
4. 
5.     public string Lastname { get; set; }
6. 
7.     public override bool Equals(object obj)
8.     {
9.         if (obj == null || GetType() != obj.GetType())
10.         {
11.             return false;
12.         }
13. 
14.         Person other = obj as Person;
15. 
16.         if (this.Firstname != other.Firstname)
17.             return false;
18. 
19.         if (this.Lastname != other.Lastname)
20.             return false;
21. 
22.         return true;
23.     }
24. 
25.     public override int GetHashCode()
26.     {
27.         return Firstname.GetHashCode() ^ Lastname.GetHashCode();
28.     }
29. 
30.     public static bool operator ==(Person person1, Person person2)
31.     {
32.         if (Object.Equals(person1, null) && Object.Equals(person2, null))
33.         {
34.             return true;
35.         }
36.         return person1.Equals(person2);
37.     }
38. 
39.     public static bool operator !=(Person person1, Person person2)
40.     {
41.         return !(person1 == person2);
42.     }
43. }

这里没有什么特别不寻常的地方。

所有 People 类都是 ObservableCollection 类的特例,类型为 Person。代码如下

1. public class People : ObservableCollection<person>
2. {
3. 
4. }
</person>

我选择 ObservableCollection 作为集合类型,因为我知道它将在显示中使用。使用 ObservableCollection 而不是从其他类型的集合复制到 ObservableCollection 会更合理,因为它具有数据绑定到它的所有优点。

ViewModel

所有 view model 的代码都可以在 ObservableCollectionSortingExample 项目中找到。

既然我们已经看到了测试和模型,让我们来看看将通过上述测试的 view model。

下面是 view model 的一部分代码。这是用于设置供视图使用的人员列表的属性。

1. People observerablePeople = new People();
2. CollectionViewSource peopleView;
3.        
4. public People People
5. {
6.      private get
7.     {
8.         return this.observerablePeople;
9.     }
10.      set
11.     {
12.          this.observerablePeople = value;
13.          peopleView = new CollectionViewSource();
14.          peopleView.Source = this.observerablePeople;
15.     }
16. }

有几个重要的代码部分需要注意。首先是第 6 行的 getprivate。这是因为为了使 ObservableCollection 可排序,您需要绑定到集合的视图。为了防止绑定到底层集合,get 被设为 private

set 用于 ObservableCollection,它是显示数据所基于的底层数据。作为 set 的一部分,您还需要更新将供视图用于显示数据的 ObservableCollection 视图。如果您不通过创建新的 CollectionViewSource 来更新视图,那么它将指向原始 ObservableCollection,因此如果 ObservableCollection 发生更改,将显示不正确的信息。

下一部分是允许您获取 ObservableCollection 视图的代码,即属性。

1. public ListCollectionView PeopleView
2. {
3.     get
4.     {                
5.         return (ListCollectionView) peopleView.View;
6.     }
7. }

这只是返回将由 View 使用的 ObservableCollection 的视图。我们返回 ListCollectionView 而不是 CollectionView,因为 ListCollectionView 提供了更好的性能,并且 CollectionViewSourceView 属性只返回一个接口。另外,因为我们知道我们使用的是 ObservableCollection,所以使用更具体的 ListCollectionView 类而不是更通用的 CollectionView 是有意义的。

类的下一部分只是一个命令属性,它由 WPF 中的 Command Binding 用于执行排序。第 9 行是将命令的方法分配给一个在命令执行时调用的事件。

1. private CommandStub sortList;
2. public ICommand SortList
3. {
4.     get
5.     {
6.         if (sortList == null)
7.         {
8.             sortList = new CommandStub();
9.             sortList.OnExecuting += 
		new CommandStub.ExecutingEventHandler(sortList_OnExecuting);
10.        }
11.        return sortList;
12.    }
13.}

下面的代码是上面设置的命令调用的实际方法。如您所见,执行排序的实际代码非常简单。

1.         void sortList_OnExecuting(object parameter)
2.         {
3.             string sortColumn = (string)parameter;
4.             this.peopleView.SortDescriptions.Clear();
5. 
6.             if (this.sortAscending)
7.             {
8.                 this.peopleView.SortDescriptions.Add
		(new SortDescription(sortColumn, ListSortDirection.Ascending));
9.                 this.sortAscending = false;
10.             }
11.             else
12.             {
13.                 this.peopleView.SortDescriptions.Add
		(new SortDescription(sortColumn, ListSortDirection.Descending));
14.                 this.sortAscending = true;
15.             }
16.         }

第 3 行计算要排序的列的名称。然后我们在第 4 行清除当前排序。

第 6 至 15 行执行实际排序。设置了一个标志来确定排序顺序是升序还是降序,然后根据升序或降序,在第 8 或 13 行将正确的视图添加到 peopleView。紧随其后的行然后切换 sortAscending 标志。

这就是 view model 的全部内容。

总而言之,您需要做的就是确保您的视图绑定到 ObservableCollection 上的 ListCollectionView,然后在视图中添加排序功能。

视图

所有视图的代码都可以在 ObservableCollectionSortingExample 项目中找到。

视图在 XAML 中定义,文件名为 PeopleView.xaml。我假设您对 XAML 有基本的了解,因此在这里不进行介绍。有关 XAML 的更多信息,您可以在本网站及其他地方找到出色的在线资源。

视图的 XAML 如下

 1. <Window x:Class="ObservableCollectionSortingExample.PeopleView"
2. xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
3. xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
4. Title="SortingExample"Height="350"Width="200"
5. xmlns:local="clr-namespace:ObservableCollectionSortingExample">
6.   <Window.Resources>
7.     <local:PeopleVewModelx:Key="PeopleViewDataContext"></local:PeopleVewModel>
8.   </Window.Resources>
9.   <Grid DataContext="{StaticResourcePeopleViewDataContext}">
10.    <Grid.RowDefinitions>
11.      <RowDefinitionHeight="*"/>
12.    </Grid.RowDefinitions>
13.    <ListView HorizontalAlignment="Stretch" Margin="10,10,10,10" Name="ListOfName"
14.       VerticalAlignment="Top" ItemsSource="{BindingPath=PeopleView}" 
		HorizontalContentAlignment="Center">
15.    <ListView.View>
16.    <GridView>
17.      <GridViewColumn DisplayMemberBinding="{BindingPath=Firstname}">
18.        <GridViewColumnHeader Command="{BindingSortList}" 
		CommandParameter="Firstname"> Firstname</GridViewColumnHeader>
19.      </GridViewColumn>
20.      <GridViewColumn DisplayMemberBinding="{BindingPath=Lastname}">
21.        <GridViewColumnHeader Command="{BindingSortList}" 
		CommandParameter="Lastname"> Lastname</GridViewColumnHeader>
22.      </GridViewColumn>
23.    </GridView>
24.    </ListView.View>
25.    </ListView>
26.   </Grid>
27. </Window>

以下是一些 XAML 中的重要行。第 7 行将此视图与 view model 的链接设置为 XAML 中的资源,然后可以在 XAML 的其余部分中使用。

第 9 行设置了 DataContext,以便控件中的所有其他控件都可以访问视图。

现在来到了第 14 行的真正神奇之处。ItemSource 设置为 view model 中的 PeopleView。正如您将记住的那样,这是对底层 ObservableCollection 列表的视图。listview 的实际列的绑定与绑定到 ObservableCollection 相同,如第 17 和 20 行所示。

第 18 和 21 行的命令绑定是我们绑定到 view model 中的 SortList 命令的地方。我们传入将要执行命令操作的列的名称。这是传递到 view model 中的 sortList_OnExecuting(object parameter) 方法的参数。此参数用于知道要按哪个列排序。

结论

因此,从上面的示例可以看出,排序的关键部分是确保您绑定到 ObservableCollection 上的 ListCollectionView,而不是直接绑定到 ObservableCollection 本身。

历史

  • 2011 年 4 月 7 日:首次发布 
© . All rights reserved.