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






4.67/5 (6投票s)
演示了如何允许绑定到 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 环境中如何实现这一需求。
本文的示例应用程序是一个人名列表,您可以按名字或姓氏进行排序。
为了说明,我们有一个未排序的列表如下

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

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

本文的范围不包括解释 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 行的 get
是 private
。这是因为为了使 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
提供了更好的性能,并且 CollectionViewSource
的 View
属性只返回一个接口。另外,因为我们知道我们使用的是 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 日:首次发布